/*
 * Copyright (c) 2023, Arm Limited and Contributors. All rights reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

#include "azure_c_shared_utility/optimize_size.h"
#include "azure_c_shared_utility/tcpsocketconnection_c.h"
#include "azure_c_shared_utility/xlogging.h"
#include "fff.h"
#include "gtest/gtest.h"
#include "iot_socket.h"

#include <cstdint>

DEFINE_FFF_GLOBALS

class TestTcpSocketConnectionOpeniot : public ::testing::Test {
public:
    TCPSOCKETCONNECTION_HANDLE socket_handle = nullptr;

    TestTcpSocketConnectionOpeniot()
    {
        RESET_FAKE(LogError);
        RESET_FAKE(iotSocketCreate);
        RESET_FAKE(iotSocketSetOpt);
        RESET_FAKE(iotSocketClose);
        RESET_FAKE(iotSocketGetHostByName);
        RESET_FAKE(iotSocketConnect);
        RESET_FAKE(iotSocketSend);
        RESET_FAKE(iotSocketRecv);
    }
};

TEST_F(TestTcpSocketConnectionOpeniot, creating_connection_fails_when_socket_cannot_be_generated)
{
    iotSocketCreate_fake.return_val = IOT_SOCKET_ESOCK;

    EXPECT_EQ(nullptr, tcpsocketconnection_create());
}

TEST_F(TestTcpSocketConnectionOpeniot, creating_connection_succeeds_when_socket_created)
{
    iotSocketCreate_fake.return_val = IOT_SOCKET_AF_INET;
    this->socket_handle = tcpsocketconnection_create();

    // If the socket is created successfully, it should be possible to connect
    EXPECT_EQ(0, tcpsocketconnection_connect(socket_handle, "", 1));
}

TEST_F(TestTcpSocketConnectionOpeniot, connecting_socket_fails_when_no_handle_passed)
{
    EXPECT_EQ(MU_FAILURE, tcpsocketconnection_connect(nullptr, "test", 40));
}

TEST_F(TestTcpSocketConnectionOpeniot, connecting_socket_fails_when_getting_hostname_fails)
{
    iotSocketCreate_fake.return_val = 0;
    this->socket_handle = tcpsocketconnection_create();

    iotSocketGetHostByName_fake.return_val = IOT_SOCKET_EINVAL;

    EXPECT_EQ(MU_FAILURE, tcpsocketconnection_connect(socket_handle, "test", 40));
}

TEST_F(TestTcpSocketConnectionOpeniot, connecting_socket_fails_when_connection_fails)
{
    iotSocketCreate_fake.return_val = 0;
    this->socket_handle = tcpsocketconnection_create();

    iotSocketGetHostByName_fake.return_val = 0;
    iotSocketConnect_fake.return_val = IOT_SOCKET_EINVAL;

    EXPECT_EQ(MU_FAILURE, tcpsocketconnection_connect(socket_handle, "test", 40));
}

TEST_F(TestTcpSocketConnectionOpeniot, connecting_socket_succeeds_when_connection_successful)
{
    iotSocketCreate_fake.return_val = 0;
    this->socket_handle = tcpsocketconnection_create();

    iotSocketGetHostByName_fake.return_val = 0;
    iotSocketConnect_fake.return_val = 0;

    EXPECT_EQ(0, tcpsocketconnection_connect(socket_handle, "test", 40));
}

TEST_F(TestTcpSocketConnectionOpeniot, connecting_socket_performs_no_action_when_socket_already_connected)
{
    // The socket will need creating and connecting firstly
    iotSocketCreate_fake.return_val = 0;
    this->socket_handle = tcpsocketconnection_create();

    iotSocketGetHostByName_fake.return_val = 0;
    iotSocketConnect_fake.return_val = 0;

    EXPECT_EQ(0, tcpsocketconnection_connect(this->socket_handle, "test", 40));

    // For this test to meet its requirements, it is neccessary to attempt a second connection, this is where no work
    // will be done.
    EXPECT_EQ(0, tcpsocketconnection_connect(this->socket_handle, "test", 40));
}

TEST_F(TestTcpSocketConnectionOpeniot, checking_if_socket_is_connected_fails_when_no_socket_handle_passed)
{
    EXPECT_FALSE(tcpsocketconnection_is_connected(nullptr));
}

TEST_F(TestTcpSocketConnectionOpeniot, checking_if_socket_is_connected_returns_true_when_socket_connected)
{
    iotSocketCreate_fake.return_val = 0;
    this->socket_handle = tcpsocketconnection_create();

    iotSocketGetHostByName_fake.return_val = 0;
    iotSocketConnect_fake.return_val = 0;

    EXPECT_EQ(0, tcpsocketconnection_connect(socket_handle, "test", 40));

    EXPECT_TRUE(tcpsocketconnection_is_connected(socket_handle));
}

TEST_F(TestTcpSocketConnectionOpeniot, checking_if_socket_is_connected_returns_false_when_socket_not_connected)
{
    iotSocketCreate_fake.return_val = 0;
    this->socket_handle = tcpsocketconnection_create();

    EXPECT_FALSE(tcpsocketconnection_is_connected(socket_handle));
}

TEST_F(TestTcpSocketConnectionOpeniot, closing_socket_fails_if_no_socket_handle_passed)
{
    iotSocketCreate_fake.return_val = 0;
    this->socket_handle = tcpsocketconnection_create();

    iotSocketGetHostByName_fake.return_val = 0;
    iotSocketConnect_fake.return_val = 0;
    tcpsocketconnection_connect(socket_handle, "test", 40);

    tcpsocketconnection_close(nullptr);

    EXPECT_TRUE(tcpsocketconnection_is_connected(this->socket_handle));
}

TEST_F(TestTcpSocketConnectionOpeniot, closing_socket_fails_if_socket_cannot_be_closed)
{
    iotSocketCreate_fake.return_val = 0;
    this->socket_handle = tcpsocketconnection_create();

    iotSocketGetHostByName_fake.return_val = 0;
    iotSocketConnect_fake.return_val = 0;
    tcpsocketconnection_connect(socket_handle, "test", 40);

    iotSocketClose_fake.return_val = IOT_SOCKET_EINVAL;
    tcpsocketconnection_close(socket_handle);

    EXPECT_TRUE(tcpsocketconnection_is_connected(this->socket_handle));
}

TEST_F(TestTcpSocketConnectionOpeniot, closing_socket_succeeds_if_socket_is_connected)
{
    iotSocketCreate_fake.return_val = 0;
    this->socket_handle = tcpsocketconnection_create();

    iotSocketGetHostByName_fake.return_val = 0;
    iotSocketConnect_fake.return_val = 0;
    tcpsocketconnection_connect(socket_handle, "test", 40);

    iotSocketClose_fake.return_val = 0;
    tcpsocketconnection_close(socket_handle);

    EXPECT_FALSE(tcpsocketconnection_is_connected(this->socket_handle));
}

TEST_F(TestTcpSocketConnectionOpeniot, sending_over_socket_fail_when_no_handle_passed)
{
    const char *data = "test";

    EXPECT_EQ(-1, tcpsocketconnection_send(nullptr, data, strlen(data)));
}

TEST_F(TestTcpSocketConnectionOpeniot, sending_over_socket_fails_if_socket_not_connected)
{
    iotSocketCreate_fake.return_val = 0;
    this->socket_handle = tcpsocketconnection_create();

    const char *data = "test";

    EXPECT_EQ(-1, tcpsocketconnection_send(socket_handle, data, strlen(data)));
}

TEST_F(TestTcpSocketConnectionOpeniot, sending_over_socket_succeeds_when_able_to_send)
{
    iotSocketCreate_fake.return_val = 0;
    this->socket_handle = tcpsocketconnection_create();

    iotSocketGetHostByName_fake.return_val = 0;
    iotSocketConnect_fake.return_val = 0;

    EXPECT_EQ(0, tcpsocketconnection_connect(this->socket_handle, "test", 40));

    const char *data = "test";

    iotSocketSend_fake.return_val = strlen(data);

    EXPECT_EQ(strlen(data), tcpsocketconnection_send(this->socket_handle, data, strlen(data)));
}

TEST_F(TestTcpSocketConnectionOpeniot, sending_all_data_over_socket_fail_when_no_handle_passed)
{
    const char *data = "test";

    EXPECT_EQ(-1, tcpsocketconnection_send_all(nullptr, data, strlen(data)));
}

TEST_F(TestTcpSocketConnectionOpeniot, sending_all_data_over_socket_fails_if_socket_not_connected)
{
    iotSocketCreate_fake.return_val = 0;
    this->socket_handle = tcpsocketconnection_create();

    const char *data = "test";

    EXPECT_EQ(-1, tcpsocketconnection_send_all(socket_handle, data, strlen(data)));
}

TEST_F(TestTcpSocketConnectionOpeniot, sending_all_data_over_socket_succeeds_when_able_to_send)
{
    iotSocketCreate_fake.return_val = 0;
    this->socket_handle = tcpsocketconnection_create();

    iotSocketGetHostByName_fake.return_val = 0;
    iotSocketConnect_fake.return_val = 0;

    EXPECT_EQ(0, tcpsocketconnection_connect(this->socket_handle, "test", 40));

    const char *data = "test";

    iotSocketSend_fake.return_val = strlen(data);

    EXPECT_EQ(strlen(data), tcpsocketconnection_send_all(this->socket_handle, data, strlen(data)));
}

TEST_F(TestTcpSocketConnectionOpeniot, receiving_data_over_socket_fail_when_no_handle_passed)
{
    char *data = (char *)0xDEADBEEF;

    EXPECT_EQ(-1, tcpsocketconnection_receive(nullptr, data, sizeof(data)));
}

TEST_F(TestTcpSocketConnectionOpeniot, receiving_data_over_socket_fails_if_socket_not_connected)
{
    iotSocketCreate_fake.return_val = 0;
    this->socket_handle = tcpsocketconnection_create();

    char *data = (char *)0xDEADBEEF;

    EXPECT_EQ(-1, tcpsocketconnection_receive(socket_handle, data, sizeof(data)));
}

TEST_F(TestTcpSocketConnectionOpeniot, receiving_data_over_socket_succeeds_when_able_to_send)
{
    iotSocketCreate_fake.return_val = 0;
    this->socket_handle = tcpsocketconnection_create();

    iotSocketGetHostByName_fake.return_val = 0;
    iotSocketConnect_fake.return_val = 0;

    EXPECT_EQ(0, tcpsocketconnection_connect(this->socket_handle, "test", 40));

    char *data = (char *)0xDEADBEEF;
    size_t data_size = sizeof(char);

    iotSocketRecv_fake.return_val = data_size;

    EXPECT_EQ(data_size, tcpsocketconnection_receive(this->socket_handle, data, data_size));
}

TEST_F(TestTcpSocketConnectionOpeniot, receiving_all_data_over_socket_fail_when_no_handle_passed)
{
    char *data = (char *)0xDEADBEEF;

    EXPECT_EQ(-1, tcpsocketconnection_receive_all(nullptr, data, sizeof(data)));
}

TEST_F(TestTcpSocketConnectionOpeniot, receiving_all_data_over_socket_fails_if_socket_not_connected)
{
    iotSocketCreate_fake.return_val = 0;
    this->socket_handle = tcpsocketconnection_create();

    char *data = (char *)0xDEADBEEF;

    EXPECT_EQ(-1, tcpsocketconnection_receive_all(socket_handle, data, sizeof(data)));
}

TEST_F(TestTcpSocketConnectionOpeniot, receiving_all_data_over_socket_succeeds_when_able_to_send)
{
    iotSocketCreate_fake.return_val = 0;
    this->socket_handle = tcpsocketconnection_create();

    iotSocketGetHostByName_fake.return_val = 0;
    iotSocketConnect_fake.return_val = 0;

    EXPECT_EQ(0, tcpsocketconnection_connect(this->socket_handle, "test", 40));

    char *data = (char *)0xDEADBEEF;
    size_t data_size = sizeof(data);

    iotSocketRecv_fake.return_val = data_size;

    EXPECT_EQ(data_size, tcpsocketconnection_receive_all(this->socket_handle, data, data_size));
}

#undef TESTTCPSOCKETCONNECTIONOPENIOT
